package com.weng.gateway.filter;

import cn.hutool.core.convert.Convert;
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.weng.cloud.utils.CloudConstants;
import com.weng.framework.core.model.ResponseData;
import com.weng.framework.web.utils.WebConstants;
import com.weng.gateway.auth.AuthCheck;
import com.weng.gateway.utils.SsoTokenLoginHelper;
import com.weng.gateway.utils.TokenUtil;
import com.weng.sso.core.constants.LoginTypeEnum;
import com.weng.sso.core.model.SsoUser;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.protocol.HTTP;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;


@Component
public class AccessFilter extends ZuulFilter {

	@Autowired
	private AuthCheck authCheck;
	
    private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);

    private final String swaggerUrlSuffix = "/v2/api-docs";
    
    @Value("${security.ignore.url}")
    private String ignoreUrls;

    @Value("${security.checkDevUrl}")
    private String checkDevUrls;

    @Value("${security.checkUseUrl}")
    private String checkUseUrls;
    
    @Override
    public String filterType() {
    	return "pre";  
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String token = TokenUtil.getToken(request);;

        if(request.getRequestURL().toString().endsWith(swaggerUrlSuffix)){
        	return null;
        }
        // 检查是否需要忽略URL的token校验
        if(checkIsIgnoreUrl(request.getServletPath())){
            return null;
        }

        // 通过token初始化当前的用户和设备信息
        CurUserAndDev curUserAndDev = initCurUserAndDev(token);
        SsoUser curUser = curUserAndDev.getCurUser();
        SsoUser curDev = curUserAndDev.getCurDev();

        // 检验是否已经登录，有些请求是要求终端登陆，有些请求是要求用户登陆，大部分请求是只要设备或用户登陆就行
        if (curDev == null && curUser == null) {
            handleNoToken(ctx,"token为空或不存在，请登陆后再访问!", 2);
            return null;
        }

        // 检查是否需要设备URL的token校验
        if(checkIsDevUrl(request.getServletPath()) && curDev == null){
            handleNoToken(ctx,"终端token为空或不存在，请登陆后再访问!", 2);
            return null;


        }
        // 检查是否需要用户登陆URL的token校验
        if(checkIsUserUrl(request.getServletPath()) && curUser == null){
            handleNoToken(ctx,"用户token为空或不存在，请登陆后再访问!", 1);
            return null;
        }
        if(curDev != null){
            setZuulRequestHeaderParamOfDev(curDev, ctx);
        }
        if(curUser != null){
            /**
             * 判断当前用户是否拥有该URL的访问权限,如果没有，则过滤，不再向下转发
             */
            String baseUrl = request.getRequestURI(); // 如/api/v1/user-center/user/list
            baseUrl = trimRequestUrl(baseUrl);
            boolean isUrlAuth = authCheck.checkUrl(baseUrl, curUser);// 判断该URL是否有访问权限
            if(!isUrlAuth){
                handleUrlNoAuth(ctx);
                return null;
            }
            setZuulRequestHeaderParam(curUser, ctx);
            return null;
        }
        return null;
    }

    /**
     *    在基于 springcloud 构建的微服务系统中，通常使用网关zuul来进行一些用户验证等过滤的操作，
     * 比如 用户在 header 或者 url 参数中存放了 token ，网关层需要 用该 token 查出用户 的 userId ，
     * 并存放于 request 中，以便后续微服务可以直接使用而避免再去用 token 查询。
     * 	    但是获取到 request，但是在 request 中只有 getParameter() 而没有 setParameter() 方法，所以直接修改 url 参数不可行，
     * 另外在 reqeust 中可以虽然可以使用 setAttribute() ,但是可能由于作用域的不同，在这里设置的 attribute 在后续的微服务中是获取不到的
     * ，因此必须考虑另外的方式。
     */
    public void setZuulRequestHeaderParam(SsoUser curUser, RequestContext ctx){

        ctx.addZuulRequestHeader(CloudConstants.HEADER_AUTH_USER_ID, curUser.getId());
        ctx.addZuulRequestHeader(CloudConstants.HEADER_AUTH_USER_ACCOUNT, curUser.getAccount());
        try {
            String trueName = curUser.getTrueName();
            trueName = trueName == null ? "" : trueName;
            ctx.addZuulRequestHeader(CloudConstants.HEADER_AUTH_USER_TRUE_NAME, URLEncoder.encode(trueName, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage(),e);
        }
    }

    /**
     *    在基于 springcloud 构建的微服务系统中，通常使用网关zuul来进行一些用户验证等过滤的操作，
     * 比如 用户在 header 或者 url 参数中存放了 token ，网关层需要 用该 token 查出用户 的 userId ，
     * 并存放于 request 中，以便后续微服务可以直接使用而避免再去用 token 查询。
     * 	    但是获取到 request，但是在 request 中只有 getParameter() 而没有 setParameter() 方法，所以直接修改 url 参数不可行，
     * 另外在 reqeust 中可以虽然可以使用 setAttribute() ,但是可能由于作用域的不同，在这里设置的 attribute 在后续的微服务中是获取不到的
     * ，因此必须考虑另外的方式。
     */
    public void setZuulRequestHeaderParamOfDev(SsoUser curUser, RequestContext ctx){

        ctx.addZuulRequestHeader(CloudConstants.HEADER_AUTH_USER_ID + "-dev", curUser.getId());
        ctx.addZuulRequestHeader(CloudConstants.HEADER_AUTH_USER_ACCOUNT + "-dev", curUser.getAccount());
        try {
            String trueName = curUser.getTrueName();
            trueName = trueName == null ? "" : trueName;
            ctx.addZuulRequestHeader(CloudConstants.HEADER_AUTH_USER_TRUE_NAME + "-dev", URLEncoder.encode(trueName, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage(),e);
        }
    }

    // 通过token初始化当前的用户和设备信息
    private CurUserAndDev initCurUserAndDev(String token){
        CurUserAndDev curUserAndDev = new CurUserAndDev();
        if(token!=null){
            try {
                String[] tokens = token.split(",");
                for(String tokenTemp :tokens){
                    SsoUser curLoginTemp = SsoTokenLoginHelper.loginCheck(tokenTemp);
                    if(curLoginTemp!=null){
                        if(curLoginTemp.getLoginType() != null && curLoginTemp.getLoginType().equals(LoginTypeEnum.LOGIN_DEV.getCode())){
                            curUserAndDev.setCurDev(curLoginTemp);
                        }else{
                            curUserAndDev.setCurUser(curLoginTemp);
                        }
                    }
                }
            } catch (Exception e) {
                logger.error("get token from cache error!please check redis service!",e);
            }
        }else{
            logger.info("token为空，请登陆后再访问!");
        }
        return curUserAndDev;
    }

    /*
     * 处理需要验交设备URL
     * @param []
     */
    private void handleNoToken(RequestContext ctx){
        ctx.setSendZuulResponse(false);
        //过滤该请求，不往下级服务去转发请求，到此结束
        ctx.setSendZuulResponse(false);
        ResponseData response = ResponseData.failedOfPermission("抱歉，你没有该操作权限!");
        ctx.setResponseBody(JSON.toJSONString(response));
        ctx.getResponse().setContentType("application/json;charset=UTF-8");
    }

    /*
     * 处理没有url权限的情况
     * @param []
     */
    private void handleUrlNoAuth(RequestContext ctx){
        ctx.setSendZuulResponse(false);
        //过滤该请求，不往下级服务去转发请求，到此结束
        ResponseData response = ResponseData.failedOfPermission("抱歉，你没有该操作权限!");
        ctx.setResponseBody(JSON.toJSONString(response));
        ctx.getResponse().setContentType("application/json;charset=UTF-8");
    }

    // 处理token为空的情况
    private void handleNoToken(RequestContext ctx, String message, Integer tokenType){
        logger.info("token为空或不存在，请登陆后再访问!");
        /**
         * 过滤器的具体逻辑。需要注意，这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求，
         * 不对其进行路由，然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码，
         * 当然我们也可以进一步优化我们的返回，比如，通过ctx.setResponseBody(body)对返回body内容进行编辑等
         */
        //过滤该请求，不往下级服务去转发请求，到此结束
        ctx.setSendZuulResponse(false);
        ResponseData response = ResponseData.failedOfTokenError(message);
        response.setData(tokenType);
        ctx.setResponseBody(JSON.toJSONString(response));
        ctx.getResponse().setContentType("application/json;charset=UTF-8");
        ctx.setResponseStatusCode(401);
				ctx.getResponse().setStatus(401);
    }

    /*
     * 检查是否需要忽略URL的token校验
     * @param [requestPath 请求地址]
     * @return true表示忽略
     */
    private boolean checkIsIgnoreUrl( String requestPath){
        String[] ignoreUrlList = ignoreUrls.split("###");
        for(String ignoreUrl:ignoreUrlList){
            if(requestPath.indexOf(ignoreUrl)>=0){
                return true;
            }
        }
        return false;
    }

    /*
     * 检查是否需要设备URL的token校验
     * @param [requestPath 请求地址]
     * @return true表示忽略
     */
    private boolean checkIsDevUrl( String requestPath){
        String[] urlList = checkDevUrls.split("###");
        for(String ignoreUrl:urlList){
            if(requestPath.indexOf(ignoreUrl)>=0){
                return true;
            }
        }
        return false;
    }

    /*
     * 检查是否需要用户登陆URL的token校验
     * @param [requestPath 请求地址]
     * @return true表示忽略
     */
    private boolean checkIsUserUrl( String requestPath){
        String[] urlList = checkUseUrls.split("###");
        for(String ignoreUrl:urlList){
            if(requestPath.indexOf(ignoreUrl)>=0){
                return true;
            }
        }
        return false;
    }

    /**
     * 裁剪请求URL，例如把/api/v1/user-center/user/list 转换为/user-center/user/list
     * @param requestUrl
     * @return
     */
    private static String trimRequestUrl(String requestUrl){
    	if(requestUrl!=null && (requestUrl.indexOf("/api/v")>=0 || requestUrl.indexOf("/api/V")>=0)){
    		requestUrl = requestUrl.replace("/api/v", "");
    		requestUrl = requestUrl.replace("/api/V", "");
    		requestUrl = requestUrl.substring(requestUrl.indexOf("/"));
    	}
    	return requestUrl;
    }
    
    // 内部类
    @Data
    class CurUserAndDev{
        private SsoUser curUser;
        private SsoUser curDev;
    }
}
